package com.rr.sc.utils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;

/**
 * No license document was available when this source code was
 * found on the internet. Hence it is considered Apache open source - all source for this
 * class is available on request.
 * 
 * Enhanced so that a full type hierarchy is checked not just immediate classes that implement
 * interfaces. Also added facility to filter out abstract classes as well.
 * 
 * This abstract class can be used to obtain a list of all classes in a
 * classpath.
 * 
 * <em>Caveat:</em> When used in environments which utilize multiple class
 * loaders--such as a J2EE Container like Tomcat--it is important to select the
 * correct classloader otherwise the classes returned, if any, will be
 * incompatible with those declared in the code employing this class lister. to
 * get a reference to your classloader within an instance method use:
 * <code>this.getClass().getClassLoader()</code> or
 * <code>Thread.currentThread().getContextClassLoader()</code> anywhere else
 * <p>
 * 
 * @author Kris Dover <krisdover@hotmail.com>
 * @version 0.2.0
 * @since 0.1.0
 */
public abstract class ClassLister {
    /**
     * Searches the classpath for all classes matching a specified search
     * criteria, returning them in a map keyed with the interfaces they
     * implement or null if they have no interfaces. The search criteria can be
     * specified via interface, package and jar name filter arguments
     * <p>
     * 
     * @param classLoader
     *            The classloader whose classpath will be traversed
     * @param interfaceFilter
     *            A Set of fully qualified interface names to search for.
     * @param packageFilter
     *            A Set of fully qualified package names to search for or or
     *            null to return classes in all packages
     * @param jarFilter
     *            A Set of jar file names to search for or null to return
     *            classes from all jars
     * @param excludeAbstractClasses
     *            If true does not include classes that are abstract in the list.
     * @return A Map of a Set of Classes keyed to their interface names
     * 
     * @throws ClassNotFoundException
     *             if the current thread's classloader cannot load a requested
     *             class for any reason
     */
    @SuppressWarnings("unchecked")
    public static Map<String, Set<Class>> findClasses(ClassLoader classLoader, Set<String> interfaceFilter, Set<String> packageFilter,
	    Set<String> jarFilter, boolean excludeAbstractClasses) throws ClassNotFoundException {
	Map<String, Set<Class>> classTable = new HashMap();
	Object[] classPaths;
	try {
	    // get a list of all classpaths
	    // System.out.println("Getting classpaths") ;

	    classPaths = ((java.net.URLClassLoader) classLoader).getURLs();
	    // System.out.println("Got " + classPaths.length + " classpaths") ;
	} catch (ClassCastException cce) {
	    // or cast failed; tokenize the system classpath
	    classPaths = System.getProperty("java.class.path", "").split(File.pathSeparator);
	    // System.out.println("ClassCastException") ;
	}

	// System.out.println("Will go through " + classPaths.length + " classpaths") ;
	for (int h = 0; h < classPaths.length; h++) {
	    Enumeration files = null;
	    JarFile module = null;
	    // for each classpath ...
	    String fName = demanglePath((URL.class).isInstance(classPaths[h]) ? ((URL) classPaths[h]).getFile() : classPaths[h].toString());

	    // System.out.println("Demangled [" + classPaths[h] + "] to [" + fName + "]") ;

	    File classPath = new File(fName);
	    if (classPath.isDirectory() && jarFilter == null) { // is our classpath a directory and jar filters are not active?
		List<String> dirListing = new ArrayList();
		// get a recursive listing of this classpath
		recursivelyListDir(dirListing, classPath, new StringBuffer());
		// an enumeration wrapping our list of files
		files = Collections.enumeration(dirListing);
	    } else if (classPath.getName().endsWith(".jar")) {
		// is our classpath a jar?
		// skip any jars not list in the filter
		if (jarFilter != null && !jarFilter.contains(classPath.getName())) {
		    continue;
		}
		module = getJarFile(classPath);
		// get an enumeration of the files in this jar
		files = module.entries();
	    }

	    // for each file path in our directory or jar
	    while (files != null && files.hasMoreElements()) {
		// get each fileName
		String fileName = files.nextElement().toString();
		// we only want the class files
		if (fileName.endsWith(".class")) {
		    // convert our full filename to a fully qualified class name
		    String className = fileName.replaceAll("/", ".").substring(0, fileName.length() - 6);

		    // debug class list
		    // System.out.println(className);
		    // skip any classes in packages not explicitly requested in
		    // our package filter, but check sub packages
		    int index = className.lastIndexOf(".");
		    String packageName = "N/A";
		    if (index != -1)
			packageName = className.substring(0, className.lastIndexOf("."));
		    if (!packageFilterContainsPackageOrParent(packageFilter, packageName)) {
			continue;
		    }
		    // get the class for our class name
		    Class theClass = null;
		    try {
			theClass = Class.forName(className, false, classLoader);
			// System.out.println("Checking [" + className + "]") ;
		    } catch (NoClassDefFoundError e) {
			// System.out.println("Skipping class '" + className +
			// "' for reason " + e.getMessage());
			continue;
		    } catch (Throwable th) {
			// ignore bad class errors
			continue;
		    }

		    // skip interfaces
		    if (theClass.isInterface()) {
			continue;
		    }
		    // Now see if this class is assignable to any of our
		    // required interfaces
		    String interfaceName = getAssignableFrom(interfaceFilter, theClass);
		    // not assignable
		    if (interfaceName == null)
			continue;
		    // now check if we need to exclude abstract classes or not
		    int modifiers = theClass.getModifiers();
		    if (((java.lang.reflect.Modifier.ABSTRACT & modifiers) == java.lang.reflect.Modifier.ABSTRACT)
			    && excludeAbstractClasses) {
			continue;
		    }

		    // is this interface already in the map?
		    if (classTable.containsKey(interfaceName)) {
			// if so then just add this class to the end of the list
			// of classes implementing this interface
			classTable.get(interfaceName).add(theClass);
		    } else {
			// else create a new list initialised with our first
			// class and put the list into the map
			Set<Class> allClasses = new HashSet<Class>();
			allClasses.add(theClass);
			classTable.put(interfaceName, allClasses);
		    }
		}
	    }

	    // close the jar if it was used
	    if (module != null) {
		try {
		    module.close();
		} catch (IOException ioe) {
		    throw new ClassNotFoundException("The module jar file '" + classPath.getName() + "' could not be closed. Error: "
			    + ioe.getMessage());
		}
	    }

	} // end for loop

	return classTable;
    } // end method

    private static boolean packageFilterContainsPackageOrParent(Set<String> packageFilter, String packageName) {
	if (packageFilter == null)
	    return true;
	// check for direct match
	if (packageFilter.contains(packageName))
	    return true;
	else {
	    // check if our package starts with something in the filter.
	    Iterator<String> iter = packageFilter.iterator();
	    while (iter.hasNext()) {
		if (packageName.startsWith(iter.next()))
		    return true;
	    }
	}
	return false;
    }

    @SuppressWarnings("unchecked")
    public static Map<String, Set<Class>> findAnnotations(ClassLoader classLoader, Set<String> packageFilter, Set<String> jarFilter)
	    throws ClassNotFoundException {
	Map<String, Set<Class>> classTable = new HashMap();
	Object[] classPaths;
	try {
	    // get a list of all classpaths
	    classPaths = ((java.net.URLClassLoader) classLoader).getURLs();
	} catch (ClassCastException cce) {
	    // or cast failed; tokenize the system classpath
	    classPaths = System.getProperty("java.class.path", "").split(File.pathSeparator);
	}

	for (int h = 0; h < classPaths.length; h++) {
	    Enumeration files = null;
	    JarFile module = null;
	    // for each classpath ...
	    String fName = demanglePath((URL.class).isInstance(classPaths[h]) ? ((URL) classPaths[h]).getFile() : classPaths[h].toString());
	    File classPath = new File(fName);
	    if (classPath.isDirectory() && jarFilter == null) { // is our classpath a directory and jar filters are not active?
		List<String> dirListing = new ArrayList();
		// get a recursive listing of this classpath
		recursivelyListDir(dirListing, classPath, new StringBuffer());
		// an enumeration wrapping our list of files
		files = Collections.enumeration(dirListing);
	    } else if (classPath.getName().endsWith(".jar")) { // is our classpath a jar?
							       // skip any jars not list in the filter
		if (jarFilter != null && !jarFilter.contains(classPath.getName())) {
		    continue;
		}
		module = getJarFile(classPath);
		// get an enumeration of the files in this jar
		files = module.entries();
	    }

	    // for each file path in our directory or jar
	    while (files != null && files.hasMoreElements()) {
		// get each fileName
		String fileName = files.nextElement().toString();
		// we only want the class files
		if (fileName.endsWith(".class")) {
		    // convert our full filename to a fully qualified class name
		    String className = fileName.replaceAll("/", ".").substring(0, fileName.length() - 6);
		    // debug class list
		    // System.out.println(className);
		    // skip any classes in packages not explicitly requested in
		    // our package filter, but check sub packages
		    int index = className.lastIndexOf(".");
		    String packageName = "N/A";
		    if (index != -1)
			packageName = className.substring(0, className.lastIndexOf("."));
		    if (!packageFilterContainsPackageOrParent(packageFilter, packageName)) {
			continue;
		    }
		    // get the class for our class name
		    Class theClass = null;
		    try {
			theClass = Class.forName(className, false, classLoader);
		    } catch (NoClassDefFoundError e) {
			// System.out.println("Skipping class '" + className +
			// "' for reason " + e.getMessage());
			continue;
		    } catch (Throwable th) {
			// ignore bad class errors
			continue;
		    }

		    // skip everything except annotations
		    if (!theClass.isAnnotation()) {
			continue;
		    }
		    String interfaceName = theClass.getName();

		    // is this interface already in the map?
		    if (classTable.containsKey(interfaceName)) {
			// if so then just add this class to the end of the list
			// of classes implementing this interface
			classTable.get(interfaceName).add(theClass);
		    } else {
			// else create a new list initialised with our first
			// class and put the list into the map
			Set<Class> allClasses = new HashSet<Class>();
			allClasses.add(theClass);
			classTable.put(interfaceName, allClasses);
		    }
		}
	    }

	    // close the jar if it was used
	    if (module != null) {
		try {
		    module.close();
		} catch (IOException ioe) {
		    throw new ClassNotFoundException("The module jar file '" + classPath.getName() + "' could not be closed. Error: "
			    + ioe.getMessage());
		}
	    }

	} // end for loop

	return classTable;
    } // end method

    @SuppressWarnings("unchecked")
    private static String getAssignableFrom(Set<String> interfaceFilter, Class clazz) {
	// goes through the filter interfaces to see if is can be assigned
	if (interfaceFilter == null)
	    return null;

	Iterator<String> iter = interfaceFilter.iterator();
	while (iter.hasNext()) {
	    Class c;
	    try {
		String interfaceName = iter.next();
		// System.out.println("Checking [" + clazz.getName() + "] against [" + interfaceName + "]") ;
		c = Class.forName(interfaceName);
		if (c.isAssignableFrom(clazz))
		    return interfaceName;
	    } catch (ClassNotFoundException ignore) {
	    }
	}
	return null;
    }

    /**
     * Recursively lists a directory while generating relative paths. This is a
     * helper function for findClasses. Note: Uses a StringBuffer to avoid the
     * excessive overhead of multiple String concatentation
     * 
     * @param dirListing
     *            A list variable for storing the directory listing as a list of
     *            Strings
     * @param dir
     *            A File for the directory to be listed
     * @param relativePath
     *            A StringBuffer used for building the relative paths
     */
    private static void recursivelyListDir(List<String> dirListing, File dir, StringBuffer relativePath) {
	int prevLen; // used to undo append operations to the StringBuffer

	// if the dir is really a directory
	if (dir.isDirectory()) {
	    // get a list of the files in this directory
	    File[] files = dir.listFiles();
	    // for each file in the present dir
	    for (int i = 0; i < files.length; i++) {
		// store our original relative path string length
		prevLen = relativePath.length();
		// call this function recursively with file list from present
		// dir and relateveto appended with present dir
		recursivelyListDir(dirListing, files[i], relativePath.append(prevLen == 0 ? "" : "/").append(files[i].getName()));
		// delete subdirectory previously appended to our relative path
		relativePath.delete(prevLen, relativePath.length());
	    }
	} else {
	    // this dir is a file; append it to the relativeto path and add it
	    // to the directory listing
	    dirListing.add(relativePath.toString());
	}
    }

    public static String demanglePath(String aPath) {
	aPath = aPath.replace("%20", " ");
	// aPath.replace("\\", File.pathSeparator) ;
	return aPath;
    }

    public static JarFile getJarFile(File file) {
	try {
	    return new JarFile(file);
	} catch (MalformedURLException mue) {
	    throw new RuntimeException("Bad classpath. Error: " + mue.getMessage());
	} catch (IOException io) {
	    throw new RuntimeException("jar file '" + file.getName() + "' could not be instantiate from file path. Error: "
		    + io.getMessage());
	}
    }
}
